]>
Commit | Line | Data |
---|---|---|
98f09799 RBR |
1 | /* |
2 | Copyright (C) 2024 Rubén Beltrán del Río | |
3 | ||
4 | This program is free software: you can redistribute it and/or modify | |
5 | it under the terms of the GNU General Public License as published by | |
6 | the Free Software Foundation, either version 3 of the License, or | |
7 | (at your option) any later version. | |
8 | ||
9 | This program is distributed in the hope that it will be useful, | |
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | GNU General Public License for more details. | |
13 | ||
14 | You should have received a copy of the GNU General Public License | |
15 | along with this program. If not, see https://map.tranquil.systems. | |
16 | */ | |
5e8ff485 RBR |
17 | import Cocoa |
18 | import SwiftUI | |
19 | ||
20 | class MapTextEditorController: NSViewController { | |
21 | ||
e2c37ac1 | 22 | @Binding var document: MapDocument |
fdb4633d | 23 | let onChange: () -> Void |
5e8ff485 | 24 | |
77d0155b RBR |
25 | private let vertexRegex = MapParsingPatterns.vertex |
26 | private let edgeRegex = MapParsingPatterns.edge | |
27 | private let blockerRegex = MapParsingPatterns.blocker | |
28 | private let opportunityRegex = MapParsingPatterns.opportunity | |
fdb4633d | 29 | private let noteRegex = MapParsingPatterns.note |
77d0155b | 30 | private let stageRegex = MapParsingPatterns.stage |
e2c37ac1 | 31 | private let groupRegex = MapParsingPatterns.group |
77d0155b | 32 | |
fdb4633d | 33 | private let changeDebouncer: Debouncer = Debouncer(seconds: 1) |
77d0155b | 34 | |
e2c37ac1 RBR |
35 | init(document: Binding<MapDocument>, onChange: @escaping () -> Void) { |
36 | self._document = document | |
fdb4633d | 37 | self.onChange = onChange |
5e8ff485 RBR |
38 | super.init(nibName: nil, bundle: nil) |
39 | } | |
40 | ||
41 | required init?(coder: NSCoder) { | |
42 | fatalError("init(coder:) has not been implemented") | |
43 | } | |
44 | ||
45 | override func loadView() { | |
46 | let scrollView = NSTextView.scrollableTextView() | |
47 | let textView = scrollView.documentView as! NSTextView | |
48 | ||
49 | scrollView.translatesAutoresizingMaskIntoConstraints = false | |
50 | ||
e2c37ac1 | 51 | textView.backgroundColor = .ui.background |
75a0e450 | 52 | textView.allowsUndo = true |
5e8ff485 | 53 | textView.delegate = self |
77d0155b | 54 | textView.textStorage?.delegate = self |
e2c37ac1 | 55 | textView.string = self.document.text |
5e8ff485 RBR |
56 | textView.isEditable = true |
57 | textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) | |
58 | self.view = scrollView | |
59 | } | |
60 | ||
61 | override func viewDidAppear() { | |
62 | self.view.window?.makeFirstResponder(self.view) | |
63 | } | |
64 | } | |
65 | ||
66 | extension MapTextEditorController: NSTextViewDelegate { | |
67 | ||
68 | func textDidChange(_ obj: Notification) { | |
69 | if let textField = obj.object as? NSTextView { | |
e2c37ac1 RBR |
70 | self.document.text = textField.string |
71 | ||
72 | changeDebouncer.debounce { | |
73 | DispatchQueue.main.async { | |
74 | self.onChange() | |
fdb4633d | 75 | } |
e2c37ac1 | 76 | } |
5e8ff485 RBR |
77 | } |
78 | } | |
79 | ||
80 | func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool | |
81 | { | |
82 | let range = Range(shouldChangeTextIn, in: view.string) | |
83 | let target = view.string[range!] | |
84 | ||
85 | if target == "--" { | |
86 | return false | |
87 | } | |
88 | ||
89 | return true | |
90 | } | |
91 | } | |
92 | ||
77d0155b | 93 | extension MapTextEditorController: NSTextStorageDelegate { |
fdb4633d | 94 | |
77d0155b RBR |
95 | override func textStorageDidProcessEditing(_ obj: Notification) { |
96 | if let textStorage = obj.object as? NSTextStorage { | |
fdb4633d | 97 | self.colorizeText(textStorage: textStorage) |
77d0155b RBR |
98 | } |
99 | } | |
100 | ||
101 | private func colorizeText(textStorage: NSTextStorage) { | |
102 | let range = NSMakeRange(0, textStorage.length) | |
103 | var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) | |
77d0155b RBR |
104 | |
105 | for match in matches { | |
e2c37ac1 RBR |
106 | textStorage.addAttributes( |
107 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) | |
108 | textStorage.addAttributes( | |
109 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
110 | textStorage.addAttributes( | |
111 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) | |
112 | textStorage.addAttributes( | |
113 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 4)) | |
77d0155b RBR |
114 | } |
115 | ||
116 | matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) | |
117 | ||
118 | for match in matches { | |
e2c37ac1 RBR |
119 | textStorage.addAttributes( |
120 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) | |
77d0155b RBR |
121 | let arrowRange = match.range(at: 2) |
122 | textStorage.addAttributes( | |
fdb4633d | 123 | [.foregroundColor: NSColor.syntax.symbol], |
77d0155b | 124 | range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) |
e2c37ac1 RBR |
125 | textStorage.addAttributes( |
126 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3)) | |
77d0155b RBR |
127 | } |
128 | ||
129 | matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) | |
130 | ||
131 | for match in matches { | |
e2c37ac1 RBR |
132 | textStorage.addAttributes( |
133 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
134 | textStorage.addAttributes( | |
135 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) | |
136 | textStorage.addAttributes( | |
137 | [.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3)) | |
138 | textStorage.addAttributes( | |
139 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 4)) | |
77d0155b RBR |
140 | } |
141 | ||
142 | matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) | |
143 | ||
144 | for match in matches { | |
e2c37ac1 RBR |
145 | textStorage.addAttributes( |
146 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
147 | textStorage.addAttributes( | |
148 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) | |
fdb4633d | 149 | } |
e2c37ac1 | 150 | |
fdb4633d RBR |
151 | matches = noteRegex.matches(in: textStorage.string, options: [], range: range) |
152 | ||
153 | for match in matches { | |
e2c37ac1 RBR |
154 | textStorage.addAttributes( |
155 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
156 | textStorage.addAttributes( | |
157 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
158 | textStorage.addAttributes( | |
159 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) | |
77d0155b RBR |
160 | } |
161 | ||
162 | matches = stageRegex.matches(in: textStorage.string, options: [], range: range) | |
163 | ||
164 | for match in matches { | |
e2c37ac1 RBR |
165 | textStorage.addAttributes( |
166 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
167 | textStorage.addAttributes( | |
168 | [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
169 | } | |
170 | ||
171 | matches = groupRegex.matches(in: textStorage.string, options: [], range: range) | |
172 | ||
173 | for match in matches { | |
174 | textStorage.addAttributes( | |
175 | [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
176 | textStorage.addAttributes( | |
177 | [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) | |
77d0155b RBR |
178 | } |
179 | } | |
180 | } | |
181 | ||
5e8ff485 RBR |
182 | struct MapTextEditor: NSViewControllerRepresentable { |
183 | ||
e2c37ac1 | 184 | @Binding var document: MapDocument |
fdb4633d | 185 | var onChange: () -> Void = {} |
5e8ff485 RBR |
186 | |
187 | func makeNSViewController( | |
188 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
189 | ) -> MapTextEditorController { | |
e2c37ac1 | 190 | return MapTextEditorController(document: $document, onChange: onChange) |
5e8ff485 RBR |
191 | } |
192 | ||
193 | func updateNSViewController( | |
194 | _ nsViewController: MapTextEditorController, | |
195 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
fdb4633d | 196 | ) {} |
5e8ff485 | 197 | } |